# :title:TJPlurker API Documentation
# encoding: utf-8
require 'oauth'
require 'json'
require 'logger'
require 'open-uri'

$tjpLog = Logger.new(STDOUT)
$tjpLog.level = Logger::INFO
$tjpLog.formatter = proc{ |severity, datetime, progname, msg|
    "#{severity} [#{datetime}] in #{progname} -- #{msg}\n"
}

module TJP
    # It's core class of TJPlurker based on {Plurk API 2.0}[http://www.plurk.com/API].
    # Note that every API wrapped methods in this class returns a JSON object
    # All mehtods will retry again when request timeout occurs.
    class TJPlurker
        # It will automatically authorize if +access_token+ and +access_token_secret+ are both given.
        # If you do not have access token and secret, leave the parameters blank,
        # and you will be asked to enter verification code to get access token and secret.
        # key:: consumer key
        # secret:: consumer secret
        # return:: OAuth::AccessToken
        def initialize(key, secret, token=nil, token_secret=nil)
            @key, @secret, @token, @token_secret = key, secret, token, token_secret
            @consumer = OAuth::Consumer.new(@key, @secret, {
                :site               => 'http://www.plurk.com',
                :scheme             => :header,
                :http_method        => :post,
                :request_token_path => '/OAuth/request_token',
                :access_token_path  => '/OAuth/access_token',
                :authorize_path     => '/OAuth/authorize'
            })
        end
        
        
        # return:: authorize URL
        def get_authorize_url
            @request_token = @consumer.get_request_token
            return @request_token.authorize_url
        end
        
        # Change or get access token and secret.
        # If parameters are not both given, user will be asked to enter verfication code,
        # otherwise, replace the access token and secret.
        # return:: access token and access token secret in Array
        def authorize(access_token=nil, access_token_secret=nil)
            if access_token && access_token_secret
                #@access_token = OAuth::AccessToken.new(@consumer, access_token, access_token_secret)
                @token, @token_secret = access_token, access_token_secret
            else
                puts "Authorize URL: #{get_authorize_url}"
                print "Please Enter your verification code: "
                verify = gets.chomp!
                access_token = @request_token.get_access_token :oauth_verifier=>verify
                @token, @token_secret = access_token.token, access_token.secret
            end
            $tjpLog.info(self.class){"Access Token: #{@token}, Access Token Secret: #{@token_secret}"}
            return @token, @token_secret
        end
        
        # url:: plurk APP url in String
        # body:: options in Hash
        # return:: result in JSON
        def api(url, body=nil, headers=nil)
            # clone consumer for supporting thread
            access_token = OAuth::AccessToken.new(@consumer.clone, @token, @token_secret)
            resp = access_token.post(url, body, headers).body
            json = JSON.parse(resp)
            $tjpLog.debug(self.class){"Response: #{json}"}
            $tjpLog.error(self.class){json["error_text"]} if json.is_a?(Hash) && json["error_text"]
            return json
        end
        
        # :section: Users
        
        # Returns info about current user's karma, including current karma,
        # karma growth, karma graph and the latest reason why the karma has dropped.
        def get_karma_stats
            method = "/APP/Users/getKarmaStats"
            api(method)
        end
        
        # :section: Profile
        
        # Returns data that's private for the current user.
        # This can be used to construct a profile and render a timeline of the latest plurks.
        def get_own_profile
            method = "/APP/Profile/getOwnProfile"
            api(method)
        end
        
        # Fetches public information such as a user's public plurks and basic information.
        # Fetches also if the current user is following the user, are friends with or is a fan.
        def get_public_profile user_id
            method = "/APP/Profile/getPublicProfile"
            attr = {:user_id=>user_id}
            api(method, attr)
        end
        
        # :section: Real Time Notifications
        
        # Get instant notifications when there are new plurks and responses on a user's timeline.
        # Generally you don't need call this because comet_channel will automatically do that for you.
        # For details, see http://www.plurk.com/API#realtime
        # return:: channel name and comet server in Hash
        def get_user_channel
            method = "/APP/Realtime/getUserChannel"
            api(method)
        end
        
        # It will do GET requests to user's comet server to get new notification data.
        # The request will sleep for about 50 seconds before returning a response if there is
        # no new data added to your channel.
        # It will automatically get comet server URL from /APP/Realtime/getUserChannel
        # if there is no comet server yet, and renew +offset+ each time it was called.
        # You don't need to worry about URL and +offset+.
        # For details, see http://www.plurk.com/API#realtime
        # return:: plurk notification in Hash
        def comet_channel
            #Assign new channel if @user_channel is nil
            @user_channel ||= get_user_channel
            begin
                res = Net::HTTP.get_response(URI.parse(@user_channel['comet_server']))
                json = JSON.parse(res.body.sub!(/CometChannel\.scriptCallback\((.*)\);/){|match| $1})
            rescue Timeout::Error => ex
                $tjpLog.warn(self.class){"Request timeout, retry."}
                retry
            rescue Errno::ECONNRESET => ex
                $tjpLog.warn(self.class){"Connection reset, retry."}
                retry
            rescue => ex
                $tjpLog.fatal(self.class){"Unknown error, skip."}
                puts ex.class, ex.message, ex.backtrace
                return
            end
            #Update the offset
            @user_channel['comet_server'].sub!(/[\d]+$/, json["new_offset"].to_s)
            if json['data']
                json['data'].each{|member|
                    notification = NotificationData.new(member)
                    # Prevent duplicate notification
                    return if notification.id == @id_buffer
                    $tjpLog.info(self.class){"Notification: #{notification.plurk_id}: \"#{notification.content}\""}
                    @id_buffer = notification.id
                    yield(notification)
                }
            end
            json
        end
        
        # :section: Polling
        
        # You should use this call to find out if there any new plurks posted to the user's timeline.
        # It's much more efficient than doing it with timeline_get_plurks, so please use it :)
        # offset:: Return plurks newer than offset, formatted as 2009-6-20T21:55:34 in String or in Time.
        # limit:: The max number of plurks to be returned (default: 20)
        def get_plurks offset, limit=nil
            method = "/APP/Polling/getPlurks"
            offset = offset.utc.strftime("%Y-%m-%dT%H:%M:%S") if offset.is_a? Time
            attr = {:offset=>offset, :limit=>limit}
            api(method, attr)
        end
        
        # Use this call to find out if there are unread plurks on a user's timeline.
        def get_unread_count
            method = "/APP/Polling/getUnreadCount"
            api(method)
        end
        
        # :section: Timeline
        
        # Get one plurk from user's timeline
        # plurk_id:: Can be Integer or String
        def get_plurk plurk_id
            method = "/APP/Timeline/getPlurk"
            attr = {:plurk_id=>plurk_id}
            api(method, attr)
        end
        
        # Get plurks from user's timeline
        # offset:: Return plurks older than offset, formatted as 2009-6-20T21:55:34 in String or in Time.
        # limit:: The max number of plurks to be returned (default: 20)
        # filter:: Can be only_user, only_responded, only_private or only_favorite
        def timeline_get_plurks offset=nil, limit=nil, filter=nil
            method = "/APP/Timeline/getPlurks"
            offset = offset.utc.strftime("%Y-%m-%dT%H:%M:%S") if offset.is_a? Time
            attr = {:offset=>offset, :limit=>limit, :filter=>filter}
            api(method, attr)
        end
        
        # Get unread plurks from user's timeline
        # offset:: Return plurks newer than offset, formatted as 2009-6-20T21:55:34 in String or in Time.
        # limit:: The max number of plurks to be returned (default: 20)
        # filter:: Can be only_user, only_responded, only_private or only_favorite
        def get_unread_plurks offset=nil, limit=nil, filter=nil
            method = "/APP/Timeline/getUnreadPlurks"
            offset = offset.utc.strftime("%Y-%m-%dT%H:%M:%S") if offset.is_a? Time
            attr = {:offset=>offset, :limit=>limit, :filter=>filter}
            api(method, attr)
        end
        
        # Get public plurks from user's timeline
        # user_id:: The user_id of the public plurks owner to get. Can be integer (like 34) or nick name (like amix).
        # offset:: Return plurks newer than offset, formatted as 2009-6-20T21:55:34 in String or in Time.
        # limit:: The max number of plurks to be returned (default: 20)
        # filter:: Can be only_user, only_responded, only_private or only_favorite
        def get_public_plurks user_id, offset=nil, limit=nil, filter=nil
            method = "/APP/Timeline/getPublicPlurks"
            offset = offset.utc.strftime("%Y-%m-%dT%H:%M:%S") if offset.is_a? Time
            attr = {:user_id=>user_id, :offset=>offset, :limit=>limit, :filter=>filter}
            api(method, attr)
        end
        
        # qualifier:: The Plurk's qualifier, must be in English. Can be following:
        #              loves, likes, shares, gives, hates, wants, has, will, asks, wishes, was,
        #              feels, thinks, says, is, :, freestyle, hopes, needs, wonders
        # limited_to:: Limit the plurk only to some users (also known as private plurking).
        #              Should can be a JSON list of friend ids in String formatting or Ruby Array, e.g.
        #              \[3,4,66,34] or "\[3,4,66,34]" will only be plurked to these user ids.
        #              If it's [0] then the Plurk is privatley posted to the poster's friends.
        # no_comments:: If set to 1 or "1", then responses are disabled for this plurk.
        #               If set to 2 or "2", then only friends can respond to this plurk.
        def plurk_add content, qualifier=':', limited_to=nil, no_comments=nil, lang='tr_ch'
            method = "/APP/Timeline/plurkAdd"
            limited_to = limited_to.to_s if limited_to.is_a? Array
            attr = {:content=>content, :qualifier=>qualifier, :limited_to=>limited_to, :no_comments=>no_comments, :lang=>lang}
            api(method, attr)
        end
        
        # plurk_id:: The id of the plurk.
        def plurk_delete plurk_id
            method = "/APP/Timeline/plurkDelete"
            attr = {:plurk_id=>plurk_id}
            api(method, attr)
        end
        
        # plurk_id:: The id of the plurk.
        # content:: The content of plurk.
        def plurk_edit plurk_id, content
            method = "/APP/Timeline/plurkEdit"
            attr = {:plurk_id=>plurk_id, :content=>content}
            api(method, attr)
        end
        
        # ids:: The plurk ids, formated as JSON String or Ruby Array, e.g. "[342,23242,2323]" or [342,23242,2323]
        def mute_plurks ids
            method = "/APP/Timeline/mutePlurks"
            ids = ids.to_s if ids.is_a? Array
            attr = {:ids=>ids}
            api(method, attr)
        end
        
        # ids:: The plurk ids, formated as JSON String or Ruby Array, e.g. "[342,23242,2323]" or [342,23242,2323]
        def unmute_plurks
            method = "/APP/Timeline/unmutePlurks"
            ids = ids.to_s if ids.is_a? Array
            attr = {:ids=>ids}
            api(method, attr)
        end
        
        # ids:: The plurk ids, formated as JSON String or Ruby Array, e.g. "[342,23242,2323]" or [342,23242,2323]
        def favorite_plurks ids
            method = "/APP/Timeline/favoritePlurks"
            ids = ids.to_s if ids.is_a? Array
            attr = {:ids=>ids}
            api(method, attr)
        end
        
        # ids:: The plurk ids, formated as JSON String or Ruby Array, e.g. "[342,23242,2323]" or [342,23242,2323]
        def unfavorite_plurks ids
            method = "/APP/Timeline/unfavoritePlurks"
            ids = ids.to_s if ids.is_a? Array
            attr = {:ids=>ids}
            api(method, attr)
        end
        
        # ids:: The plurk ids, formated as JSON String or Ruby Array, e.g. "[342,23242,2323]" or [342,23242,2323]
        # note_position:: If true responses_seen of the plurks will be updated as well (to match response_count).
        def mark_as_read ids, note_position=nil
            method = "/APP/Timeline/markAsRead"
            ids = ids.to_s if ids.is_a? Array
            attr = {:ids=>ids, :note_position=>note_position}
            api(method, attr)
        end
        
        # :section: Response
        
        # Fetches responses for plurk with plurk_id and some basic info about the users.
        # plurk_id:: The plurk that the responses belong to.
        # from_response:: Only fetch responses from an offset - could be 5, 10 or 15 (default: 0)
        def response_get plurk_id, from_response=nil
            method = "/APP/Responses/get"
            attr = {:plurk_id=>plurk_id, :from_response=>from_response}
            api(method, attr)
        end
        
        # Adds a responses to plurk_id. Language is inherited from the plurk.
        # plurk_id:: The plurk that the responses belong to.
        # content:: The response's text.
        # qualifier:: The Plurk's qualifier, must be in English. Refers to plurk_add.
        def response_add plurk_id, content, qualifier=':'
            method = "/APP/Responses/responseAdd"
            attr = {:plurk_id=>plurk_id, :content=>content, :qualifier=>qualifier}
            api(method, attr)
        end
        
        #Deletes a response. A user can delete own responses or responses that are posted to own plurks.
        # response_id:: The id of the response to delete.
        # plurk_id:: The plurk that the response belongs to.
        def response_delete response_id, plurk_id
            method = "/APP/Responses/responseDelete"
            attr = {:response_id=>response_id, :plurk_id=>plurk_id}
            api(method, attr)
        end
        
        # :section: Friends and Fans
        
        # Returns user_id's friend list in chucks of 100 friends at a time.
        # However, We suggest you use get_friends instead of get_friends_by_offset
        # limit:: The max number of friends to be returned.
        # offset:: Can be Numeric or String, e.g. 10, "20", 30, "40" etc.
        # return:: An Array
        def get_friends_by_offset user_id, limit=100, offset=0
            method = "/APP/FriendsFans/getFriendsByOffset"
            attr = {:user_id=>user_id, :offset=>offset, :limit=>limit}
            api(method, attr)
        end
        
        # Returns user_id's fans list in chucks of 100 fans at a time.
        # However, We suggest you use get_fans instead of get_fans_by_offset
        # limit:: The max number of friends to be returned.
        # offset:: Can be Numeric or String, e.g. 10, "20", 30, "40" etc.
        # return:: An Array
        def get_fans_by_offset user_id, limit=100, offset=0
            method = "/APP/FriendsFans/getFansByOffset"
            attr = {:user_id=>user_id, :offset=>offset, :limit=>limit}
            api(method, attr)
        end
        
        # Returns users that the current logged in user follows as fan - in chucks of 100 fans at a time.
        # However, We suggest you use get_following instead of get_following_by_offset
        # limit:: The max number of friends to be returned.
        # offset:: Can be Numeric or String, e.g. 10, "20", 30, "40" etc.
        # return:: An Array
        def get_following_by_offset limit=100, offset=0
            method = "/APP/FriendsFans/getFollowingByOffset"
            attr = {:offset=>offset, :limit=>limit}
            api(method, attr)
        end
        
        # Create a friend request to friend_id. User with friend_id has to accept a friendship.
        # friend_id:: The ID of the user you want to befriend.
        def become_friend friend_id
            method = "/APP/FriendsFans/becomeFriend"
            attr = {:friend_id=>friend_id}
            api(method, attr)
        end
        
        # Remove friend with ID friend_id. friend_id won't be notified.
        # friend_id:: The ID of the user you want to remove
        def remove_as_friend friend_id
            method = "/APP/FriendsFans/removeAsFriend"
            attr = {:friend_id=>friend_id}
            api(method, attr)
        end
        
        # Become fan of fan_id. To stop being a fan of someone, use set_following
        # fan_id:: The ID of the user you want to become fan of
        def become_fan fan_id
            method = "/APP/FriendsFans/becomeFan"
            attr = {:fan_id=>fan_id}
            api(method, attr)
        end
        
        # Update following of user_id. A user can befriend someone, but can unfollow them.
        # This request is also used to stop following someone as a fan.
        # user_id:: The ID of the user you want to follow/unfollow
        # follow:: Normally, true if the user should be followed, and false if the user should be unfollowed.
        #          Actually, toggle the follwing state no matter you give true or false
        def set_following user_id, follow=true
            method = "/APP/FriendsFans/setFollowing"
            attr = {:user_id=>user_id, :follow=>follow}
            api(method, attr)
        end
        
        # Returns a JSON object of the logged in users friends (nick name and full name).
        # This information can be used to construct auto-completion for private plurking.
        # Notice that a friend list can be big, depending on how many friends a user has,
        # so this list should be lazy-loaded in your application.
        def get_completion
            method = "/APP/FriendsFans/getCompletion"
            api(method)
        end
        
        # :section: Alerts
        # ===General data structures
        # The data returned by getActive and getHistory can be of following nature:
        # Friendship request: 
        #  {"type": "friendship_request", "from_user": {"nick_name": ...}, "posted": ...} 
        #
        # Friendship pending: 
        #  {"type": "friendship_pending", "to_user": {"nick_name": ...}, "posted": ...} 
        #
        # New fan notification: (does not require actions from the user) 
        #  {"type": "new_fan", "new_fan": {"nick_name": ...}, "posted": ...} 
        #
        # Friendship accepted notification: (does not require actions from the user) 
        #  {"type": "friendship_accepted", "friend_info": {"nick_name": ...}, "posted": ...} 
        #
        # New friend notification: (does not require actions from the user) 
        #  {"type": "new_friend", "new_friend": {"nick_name": ...}, "posted": ...} 
        
        # Return a JSON list of current active alerts.
        def get_active
            method = "/APP/Alerts/getActive"
            api(method)
        end
        
        # Return a JSON list of past 30 alerts.
        def get_history
            method = "/APP/Alerts/getHistory"
            api(method)
        end
        
        # user_id:: The user_id that has asked for friendship.
        def add_as_fan user_id
            method = "/APP/Alerts/addAsFan"
            attr = {:user_id=>user_id}
            api(method, attr)
        end
        
        # Accept all friendship requests as fans.
        def add_all_as_fan
            method = "/APP/Alerts/addAllAsFan"
            api(method)
        end
        
        # Accept all friendship requests as friends.
        def add_all_as_friends
            method = "/APP/Alerts/addAllAsFriends"
            api(method)
        end
        
        # Accept user_id as friend.
        # user_id:: The user_id that has asked for friendship.
        def add_as_friend user_id
            method = "/APP/Alerts/addAsFriend"
            attr = {:user_id=>user_id}
            api(method, attr)
        end
        
        # Deny friendship to user_id.
        # user_id:: The user_id that has asked for friendship.
        def deny_friendship user_id
            method = "/APP/Alerts/denyFriendship"
            attr = {:user_id=>user_id}
            api(method, attr)
        end
        
        # Remove notification to user with id user_id.
        # user_id:: The user_id that the current user has requested friendship for.
        def remove_notification user_id
            method = "/APP/Alerts/removeNotification"
            attr = {:user_id=>user_id}
            api(method, attr)
        end
        
        # :section: Search
        
        # Returns the latest 20 plurks on a search term.
        # query:: The query after Plurks.
        # offset:: A plurk_id of the oldest Plurk in the last search result.
        def plurk_search query, offset=nil
            method = "/APP/PlurkSearch/search"
            attr = {:query=>query, :offset=>offset}
            api(method, attr)
        end
        
        # Returns 10 users that match query, users are sorted by karma.
        # query:: The query after users.
        # offset:: Page offset, like 10, 20, 30 etc.
        def user_search query, offset=0
            method = "/APP/UserSearch/search"
            attr = {:query=>query, :offset=>offset}
            api(method, attr)
        end
        
        # :section: Emoticons
        
        # Emoticons are a big part of Plurk since they make it easy to express feelings.
        # Check out current Plurk emoticons. This call returns a JSON object that looks like:
        #  {"karma": {"0": [[":-))", "http:\/\/statics.plurk.com\/XXX.gif"], ...], ...},
        #  "recuited": {"10": [["(bigeyes)", "http:\/\/statics.plurk.com\/XXX.gif"], ...], ...} }
        # emoticons["karma"][25] denotes that the user has to have karma over 25 to use these emoticons.
        # emoticons["recuited"][10] means that the user has to have user.recuited >= 10 to use these emoticons.
        # It's important to check for these things on the client as well,
        # since the emoticon levels are checked in the models.
        def emoticons_get
            method = "/APP/Emoticons/get"
            api(method)
        end
        
        # :section: Blocks
        
        # offset:: What page should be shown, e.g. 0, 10, 20.
        def blocks_get offset=0
            method = "/APP/Blocks/get"
            attr = {:offset=>offset}
            api(method, attr)
        end
        
        # user_id:: The id of the user that should be blocked.
        def block user_id
            method = "/APP/Blocks/block"
            attr = {:user_id=>user_id}
            api(method, attr)
        end
        
        # user_id: The id of the user that should be unblocked.
        def unblock user_id
            method = "/APP/Blocks/unblock"
            attr = {:user_id=>user_id}
            api(method, attr)
        end
        
        # :section: Cliques
        
        def get_cliques
            method = "/APP/Cliques/getCliques"
            api(method)
        end
        
        # clique_name:: The name of the new clique
        def get_clique clique_name
            method = "/APP/Cliques/getClique"
            attr = {:clique_name=>clique_name}
            api(method, attr)
        end
        
        # clique_name:: The name of the new clique
        def create_clique clique_name
            method = "/APP/Cliques/createClique"
            attr = {:clique_name=>clique_name}
            api(method, attr)
        end
        
        # clique_name:: The name of the clique to rename
        # new_name:: The name of the new clique
        def rename_clique clique_name, new_name
            method = "/APP/Cliques/renameClique"
            attr = {:clique_name=>clique_name, :new_name=>new_name}
            api(method, attr)
        end
        
        # clique_name:: The name of the clique
        # user_id:: The user to add to the clique
        def cliques_add clique_name, user_id
            method = "/APP/Cliques/add"
            attr = {:clique_name=>clique_name, :user_id=>user_id}
            api(method, attr)
        end
        
        # clique_name:: The name of the clique
        # user_id:: The user to remove from the clique
        def cliques_remove clique_name, user_id
            method = "/APP/Cliques/remove"
            attr = {:clique_name=>clique_name, :user_id=>user_id}
            api(method, attr)
        end
        
        # :section: PlurkTop
        
        def get_collections
            method = "/APP/PlurkTop/getCollections"
            api(method)
        end
        
        # collection_name:: only get plurks in specified collection
        # offset:: offset of Plurks in PlurkTop, should be an float, e.g. 0.99.
        # limit:: number of plurks returned (default: 30)
        # sorting:: the way to sort plurks in PlurkTop, can be "hot" for sorting by popularity or "new" for posted time.
        def plurk_top_get_plurks collection_name, limit=nil, offset=nil, sorting=nil
            method = "/APP/PlurkTop/getPlurks"
            attr = {:collection_name=>collection_name, :limit=>limit, :offset=>offset, :sorting=>sorting}
            api(method, attr)
        end
        
        # :section: TJPlurker Provided
        # Those methos were all provided by TJPlukrer instead of Plurk
        
        # Improved from get_friends_by_offset for getting all friends from a user
        # return:: An Array
        def get_friends user_id, at_least=nil
            list = []
            offset = 0;
            loop{
                set = get_friends_by_offset(user_id, 100, offset)
                size = set.size
                break if size == 0
                list |= set
                break if at_least && list.size>=at_least
                offset += size
            }
            return list
        end
        
        # Improved from get_fans_by_offset for getting all fans from a user
        # return:: An Array
        def get_fans user_id, at_least=nil
            list = []
            offset = 0;
            loop{
                set = get_fans_by_offset(user_id, 100, offset)
                size = set.size
                break if size == 0
                list |= set
                break if at_least && list.size>=at_least
                offset += size
            }
            return list
        end
        
        # Improved from get_following_by_offset for getting all fans from a user
        # return:: An Array
        def get_following at_least=nil
            list = []
            offset = 0;
            loop{
                set = get_following_by_offset(100, offset)
                size = set.size
                break if size == 0
                list |= set
                break if at_least && list.size>=at_least
                offset += size
            }
            return list
        end
        
        # Used to follow a lot of users.
        # level:: e.g. 1 for following user's all friends, 2 for following user's all friends and all friends of all friends.
        def super_become_fan fan_id, level=1
            return if level<=0
            become_fan(fan_id)
            list = []
            offset = 0;
            loop{
                set = get_friends_by_offset(fan_id, 100, offset)
                size = set.size
                break if size == 0
                tg = ThreadGroup.new
                set.each{|i|
                    tg.add Thread.new{
                        wait_sec = 1
                        until become_fan(i["uid"]).key?("success_text")
                            $tjpLog.info(self.class){"Trying to become #{i["uid"]}'s fan in #{wait_sec} seconds"}
                            sleep(wait_sec)
                            wait_sec *= 2
                        end
                        $tjpLog.info(self.class){"Become #{i["uid"]}'s fan successfully"}
                    }
                }
                tg.list.each{|i| i.join }
                list |= set
                offset += size
            }
            list.each{|i|
                super_become_fan(i["uid"], level-1)
            }
        end
    end
    
    # Format plurk notification. See TJPlurker#comet_channel
    class NotificationData
        # Can be +new_plurk+ or +new_response+.
        attr_reader :type
        attr_reader :response_count
        attr_reader :plurk_id
        attr_reader :lang
        attr_reader :user_id
        attr_reader :qualifier
        attr_reader :content
        # Notification ID.
        attr_reader :id
        # Time of the listened post.
        attr_reader :posted
        attr_reader :owner_id
        # The original JSON object from server response.
        attr_reader :origin
        
        def initialize member
            @type = member['type']
            @response_count = member['response_count']
            @plurk_id = member['plurk_id']
            
            @lang = @type=="new_plurk" ? member['lang'] : member['response']['lang']
            @user_id = @type=="new_plurk" ? member['user_id'] : member['response']['user_id']
            @qualifier = @type=="new_plurk" ? member['qualifier'] : member['response']['qualifier']
            @content = @type=="new_plurk" ? member['content'] : member['response']['content']
            @id = @type=="new_plurk" ? member['id'] : member['response']['id']
            @posted = @type=="new_plurk" ? member['posted'] : member['response']['posted']
            
            @owner_id = @type=="new_plurk" ? member['owner_id'] : member['plurk']['owner_id']
            
            @origin = member
        end
    end
    
    module_function
    # Convert user name into user_id, e.g. Given "tonytonyjan", return 5874158 in Integer
    # return:: user id in Integer
    def get_uid user_name
        open("http://www.plurk.com/#{user_name}").read =~ /"user_id": (\d*)/
        raise "User \"#{user_name}\" not found." unless $1
        return $1.to_i
    end
end